/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.hop.core;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.InvalidPropertiesFormatException;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.apache.commons.collections4.IteratorUtils;

/** Thread Safe version of Java Properties class. */
public class ConcurrentMapProperties extends Properties {

  private static final long serialVersionUID = -7444528393201025496L;

  protected ConcurrentMap<Object, Object> storageMap = new ConcurrentHashMap<>();

  public ConcurrentMapProperties() {
    this(null);
  }

  public ConcurrentMapProperties(Properties defaults) {
    if (defaults != null) {
      if (defaults instanceof ConcurrentMapProperties) {
        this.defaults = defaults;
      } else {
        this.defaults = convertProperties(defaults);
      }
    }
  }

  @Override
  public synchronized Object put(Object key, Object value) {
    return storageMap.put(key, value);
  }

  @Override
  public synchronized Object remove(Object key) {
    return storageMap.remove(key);
  }

  @Override
  public synchronized void clear() {
    storageMap.clear();
  }

  @Override
  public synchronized Object clone() {
    ConcurrentMapProperties cloned = new ConcurrentMapProperties();
    cloned.putAll(storageMap);
    return cloned;
  }

  @Override
  public boolean containsValue(Object value) {
    return storageMap.containsValue(value);
  }

  @Override
  public synchronized Object get(Object key) {
    return storageMap.get(key);
  }

  @Override
  public synchronized Object compute(
      Object key, BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
    return storageMap.compute(key, remappingFunction);
  }

  @Override
  public synchronized Object computeIfAbsent(
      Object key, Function<? super Object, ? extends Object> mappingFunction) {
    return storageMap.computeIfAbsent(key, mappingFunction);
  }

  @Override
  public synchronized Object computeIfPresent(
      Object key, BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
    return storageMap.computeIfPresent(key, remappingFunction);
  }

  @Override
  public synchronized boolean contains(Object value) {
    return storageMap.containsValue(value);
  }

  @Override
  public synchronized boolean isEmpty() {
    return storageMap.isEmpty();
  }

  @Override
  public synchronized int size() {
    return storageMap.size();
  }

  @Override
  public synchronized boolean containsKey(Object key) {
    return storageMap.containsKey(key);
  }

  @Override
  public synchronized Enumeration<Object> elements() {
    return (Enumeration<Object>) IteratorUtils.asEnumeration(storageMap.values().iterator());
  }

  @Override
  public Set<java.util.Map.Entry<Object, Object>> entrySet() {
    return storageMap.entrySet();
  }

  @Override
  public synchronized void forEach(BiConsumer<? super Object, ? super Object> action) {
    storageMap.forEach(action);
  }

  @Override
  public synchronized Object getOrDefault(Object key, Object defaultValue) {
    return storageMap.getOrDefault(key, defaultValue);
  }

  @Override
  public synchronized Enumeration<Object> keys() {
    return (Enumeration<Object>) IteratorUtils.asEnumeration(storageMap.keySet().iterator());
  }

  @Override
  public Set<Object> keySet() {
    return storageMap.keySet();
  }

  @Override
  public synchronized Object merge(
      Object key,
      Object value,
      BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
    return storageMap.merge(key, value, remappingFunction);
  }

  @Override
  public synchronized void putAll(Map<? extends Object, ? extends Object> t) {
    storageMap.putAll(t);
  }

  @Override
  public synchronized Object putIfAbsent(Object key, Object value) {
    return storageMap.putIfAbsent(key, value);
  }

  @Override
  public synchronized boolean remove(Object key, Object value) {
    return storageMap.remove(key, value);
  }

  @Override
  public synchronized boolean replace(Object key, Object oldValue, Object newValue) {
    return storageMap.replace(key, oldValue, newValue);
  }

  @Override
  public synchronized void replaceAll(
      BiFunction<? super Object, ? super Object, ? extends Object> function) {
    storageMap.replaceAll(function);
  }

  @Override
  public synchronized Object replace(Object key, Object value) {
    return storageMap.replace(key, value);
  }

  @Override
  public Collection<Object> values() {
    return storageMap.values();
  }

  @Override
  public String getProperty(String key) {
    Object oval = storageMap.get(key);
    String sval = (oval instanceof String) ? (String) oval : null;
    return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
  }

  @Override
  public String getProperty(String key, String defaultValue) {
    /*
     * This method simply uses getProperty(String key), so it is fine to use the original implementation
     */
    return super.getProperty(key, defaultValue);
  }

  @Override
  public synchronized Object setProperty(String key, String value) {
    return storageMap.put(key, value);
  }

  @Override
  public synchronized boolean equals(Object o) {
    super.putAll(storageMap);
    boolean result = super.equals(o);
    super.clear();
    return result;
  }

  @Override
  public synchronized String toString() {
    super.putAll(storageMap);
    String result = super.toString();
    super.clear();
    return result;
  }

  @Override
  public synchronized int hashCode() {
    super.putAll(storageMap);
    int result = super.hashCode();
    super.clear();
    return result;
  }

  @Override
  public synchronized Enumeration<?> propertyNames() {
    return IteratorUtils.asEnumeration(stringPropertyNames().iterator());
  }

  @Override
  public synchronized Set<String> stringPropertyNames() {
    Set<String> copiedSet = new HashSet<>();
    if (defaults != null) {
      defaults.keySet().forEach(x -> copiedSet.add((String) x));
    }
    storageMap.keySet().forEach(x -> copiedSet.add((String) x));
    return copiedSet;
  }

  @Override
  public synchronized void list(PrintStream out) {
    super.putAll(storageMap);
    super.list(out);
    super.clear();
  }

  @Override
  public synchronized void list(PrintWriter out) {
    super.putAll(storageMap);
    super.list(out);
    super.clear();
  }

  @Override
  public synchronized void load(InputStream inStream) throws IOException {
    super.putAll(storageMap);
    super.load(inStream);
    super.forEach((key, value) -> storageMap.put(key, value));
    super.clear();
  }

  @Override
  public synchronized void load(Reader reader) throws IOException {
    super.putAll(storageMap);
    super.load(reader);
    super.forEach((key, value) -> storageMap.put(key, value));
    super.clear();
  }

  @Override
  public synchronized void loadFromXML(InputStream in)
      throws IOException, InvalidPropertiesFormatException {
    super.putAll(storageMap);
    super.loadFromXML(in);
    super.forEach((key, value) -> storageMap.putIfAbsent(key, value));
    super.clear();
  }

  @Override
  public synchronized void store(OutputStream out, String comments) throws IOException {
    super.putAll(storageMap);
    super.store(out, comments);
    super.clear();
  }

  @Override
  public synchronized void store(Writer writer, String comments) throws IOException {
    super.putAll(storageMap);
    super.store(writer, comments);
    super.clear();
  }

  @Override
  public synchronized void storeToXML(OutputStream os, String comment) throws IOException {
    super.putAll(storageMap);
    super.storeToXML(os, comment);
    super.clear();
  }

  @Override
  public synchronized void storeToXML(OutputStream os, String comment, String encoding)
      throws IOException {
    super.putAll(storageMap);
    super.storeToXML(os, comment, encoding);
    super.clear();
  }

  /**
   * Converts a Properties object to a ConcurrentMapProperties object
   *
   * @param props
   * @return A new ConcurrentMapProperties with all properties enumerated (including defaults)
   */
  public static ConcurrentMapProperties convertProperties(Properties props) {
    if (props != null) {
      if (!(props instanceof ConcurrentMapProperties)) {
        ConcurrentMapProperties result = new ConcurrentMapProperties(null);
        synchronized (props) {
          for (String prop : props.stringPropertyNames()) {
            result.put(prop, props.getProperty(prop));
          }
        }
        return result;
      } else {
        // Already a ConcurrentMapProperties
        return (ConcurrentMapProperties) props;
      }
    }
    return null;
  }
}
